Mapping NYC’s Urban Forest

A Spatial Analysis of Street Trees Across City Council Districts

Author

Mirae Han

Introduction

Urban Trees and Environmental Justice

New York City’s urban forest represents one of the most valuable environmental assets for the five boroughs. With over 680,000 street trees catalogued in the 2015 Street Tree Census, these living infrastructure components provide critical ecosystem services including air quality improvement, stormwater management, urban heat island mitigation, and enhanced quality of life for residents.

However, the distribution of these environmental benefits is far from uniform. Understanding which neighborhoods and council districts have robust tree canopy coverage—and which face tree deserts—has important implications for environmental justice and urban planning policy.

Research Questions

This analysis examines NYC’s street tree distribution across City Council districts to answer five key questions:

  1. Which council district has the most trees? Identifying areas with strong tree infrastructure
  2. Which district has the highest tree density? Accounting for district size to find true tree coverage
  3. Where are trees most at risk? Finding districts with the highest proportion of dead or dying trees
  4. What trees thrive in Manhattan? Understanding species distribution in the urban core
  5. What’s nearest to Baruch College? A hyperlocal exploration of campus tree diversity

Data Sources

NYC Street Tree Census (2015)
- Source: NYC Parks Department via NYC Open Data Portal
- Records: ~680,000+ street trees
- Attributes: Species, health condition, location coordinates, tree dimensions

NYC City Council District Boundaries
- Source: NYC Department of City Planning
- 51 council districts across five boroughs
- Includes district area for density calculations

Data Acquisition

Task 1: Loading Council District Boundaries

Show code
#' Load NYC City Council District Boundaries
get_nyc_council_districts <- function() {
  
  data_dir <- file.path("data", "mp03")
  
  # Find existing ZIP file
  zip_file <- list.files(data_dir, pattern = "\\.zip$", full.names = TRUE)
  
  if (length(zip_file) == 0) {
    stop("No ZIP file found in data/mp03. Please download and place it there.")
  }
  
  zip_file <- zip_file[1]
  
  # Unzip if needed
  unzip_dir <- file.path(data_dir, file_path_sans_ext(basename(zip_file)))
  
  if (!dir.exists(unzip_dir)) {
    unzip(zip_file, exdir = unzip_dir)
  }
  
  # Find and read shapefile
  shp_file <- list.files(unzip_dir, pattern = "\\.shp$", 
                         full.names = TRUE, recursive = TRUE)[1]
  
  if (is.na(shp_file)) {
    stop("Could not find a .shp file inside the ZIP.")
  }
  
  # Read and transform to WGS84
  st_read(shp_file, quiet = TRUE) |>
    st_transform(crs = "WGS84")
}

# Load data
NYC_CC <- get_nyc_council_districts()

# Create simplified version for visualization
NYC_CC_SIMP <- NYC_CC |>
  mutate(geometry = st_simplify(geometry, dTolerance = 5, preserveTopology = TRUE))

The council district shapefile contains 51 districts covering all five boroughs. We simplified the geometry for faster rendering while preserving topological relationships.

Task 2: Downloading Tree Census Data

Show code
#' Download NYC Street Tree Data via API
get_nyc_tree_points <- function(limit = 50000) {
  
  data_dir <- file.path("data", "mp03")
  
  base_req <- request("https://data.cityofnewyork.us") |>
    req_url_path_append("resource") |>
    req_url_path_append("hn5i-inap.geojson") |>
    req_error(is_error = \(resp) FALSE)
  
  offset <- 0L
  page   <- 1L
  files  <- character()
  
  repeat {
    fname <- file.path(data_dir, 
                      glue("nyc_treepoints_{sprintf('%03d', page)}.geojson"))
    
    if (!file.exists(fname)) {
      resp <- base_req |>
        req_url_query(`$limit` = limit, `$offset` = offset) |>
        req_perform()
      
      if (resp_is_error(resp)) {
        stop("Tree points request failed with status ", resp_status(resp))
      }
      
      writeBin(resp_body_raw(resp), fname)
    }
    
    files <- c(files, fname)
    tmp <- st_read(fname, quiet = TRUE)
    
    if (nrow(tmp) < limit) break
    
    offset <- offset + limit
    page   <- page + 1L
  }
  
  # Combine all chunks
  map(files, ~ st_read(.x, quiet = TRUE)) |> bind_rows()
}

# Load tree dataset
NYC_TREES <- get_nyc_tree_points()

The API download retrieved 1,094,587 individual street tree records across NYC. Each record includes species identification, health status, and precise geographic coordinates.

Exploratory Visualization

Task 3: Mapping Trees and Districts

Show code
# Sample trees for visualization
TREE_SAMPLE_FULL <- st_read("data/mp03/nyc_treepoints_001.geojson", quiet = TRUE)
TREE_SAMPLE <- slice_sample(TREE_SAMPLE_FULL, n = 1000)
TREE_SAMPLE <- st_transform(TREE_SAMPLE, st_crs(NYC_CC_SIMP))

# Create map
ggplot() +
  geom_sf(
    data = NYC_CC_SIMP,
    fill = NA,
    color = "grey40",
    linewidth = 0.3
  ) +
  geom_sf(
    data = TREE_SAMPLE,
    color = "forestgreen",
    alpha = 0.7,
    size = 0.8
  ) +
  coord_sf() +
  labs(
    title = "NYC Street Trees Across City Council Districts",
    subtitle = "Random sample of 1,000 trees from 2015 census",
    caption = "Source: NYC Open Data - 2015 Street Tree Census"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 16, face = "bold"),
    plot.subtitle = element_text(hjust = 0.5, size = 11, color = "grey30"),
    plot.caption = element_text(hjust = 0, size = 8, color = "grey50"),
    axis.title = element_blank(),
    panel.grid = element_line(color = "grey90", linewidth = 0.2)
  )

Sample of 1,000 street trees across NYC council districts

This preliminary visualization reveals the geographic extent of NYC’s street tree inventory. Even from this small sample, we observe denser tree coverage in certain districts, particularly in outer borough neighborhoods with more residential street frontage.

Spatial Analysis

Task 4: Joining Trees to Districts

Show code
# Ensure matching CRS
NYC_TREES <- st_transform(NYC_TREES, st_crs(NYC_CC_SIMP))

# Perform spatial join
TREE_DIST <- st_join(NYC_TREES, NYC_CC_SIMP, join = st_intersects)

# Add borough classification
TREE_DIST <- TREE_DIST |>
  mutate(
    borough = case_when(
      CounDist >= 1  & CounDist <= 10 ~ "Manhattan",
      CounDist >= 11 & CounDist <= 18 ~ "Bronx",
      CounDist >= 19 & CounDist <= 32 ~ "Queens",
      CounDist >= 33 & CounDist <= 48 ~ "Brooklyn",
      CounDist >= 49 & CounDist <= 51 ~ "Staten Island",
      TRUE ~ NA_character_
    )
  )

Successfully matched 1,094,685 trees to their corresponding council districts using spatial intersection.

Analysis Results

Q1: District with Most Trees

Show code
TREES_BY_DIST <- TREE_DIST |>
  st_drop_geometry() |>
  group_by(CounDist, borough) |>
  summarise(n_trees = n(), .groups = "drop") |>
  arrange(desc(n_trees))

DIST_MOST_TREES <- TREES_BY_DIST |> slice(1)

# Create table
datatable(
  head(TREES_BY_DIST, 10),
  caption = "Top 10 Council Districts by Tree Count",
  options = list(pageLength = 10, dom = 't'),
  rownames = FALSE,
  colnames = c("District", "Borough", "Number of Trees")
) |>
  formatCurrency("n_trees", currency = "", digits = 0)

Finding: Council District 51 in Staten Island leads with 70,965 street trees.

This district’s high tree count likely reflects a combination of factors including district size, residential density with tree-lined streets, and successful tree planting initiatives. Districts in the outer boroughs tend to have higher absolute tree counts due to their larger geographic areas and lower building density, which provides more street frontage for tree pits.

Q2: Highest Tree Density

Show code
TREES_DENSITY <- TREE_DIST |>
  st_drop_geometry() |>
  group_by(CounDist, borough) |>
  summarise(
    n_trees = n(),
    area = first(Shape_Area),
    .groups = "drop"
  ) |>
  mutate(
    tree_density = n_trees / area,
    trees_per_sq_km = tree_density * 1e6  # Convert to per sq km
  ) |>
  arrange(desc(tree_density))

DIST_HIGHEST_DENSITY <- TREES_DENSITY |> slice(1)

# Visualization
top10_density <- head(TREES_DENSITY, 10)

ggplot(top10_density, aes(x = reorder(paste0("District ", CounDist), trees_per_sq_km), 
                          y = trees_per_sq_km, fill = borough)) +
  geom_col() +
  geom_text(aes(label = comma(round(trees_per_sq_km))), 
            hjust = -0.1, size = 3) +
  coord_flip() +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Top 10 Council Districts by Tree Density",
    subtitle = "Trees per square kilometer",
    x = NULL,
    y = "Trees per Square Kilometer",
    fill = "Borough"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 14, face = "bold"),
    plot.subtitle = element_text(hjust = 0.5, size = 10),
    panel.grid.major.y = element_blank()
  )

Finding: District 7 has the highest tree density at 283 trees per square kilometer.

Unlike absolute tree count, density measures reveal which districts provide the most tree coverage relative to their size. Higher density districts often feature mature residential neighborhoods with continuous street tree planting along sidewalks, creating the characteristic tree-canopy corridors that provide maximum environmental benefits.

Q3: Dead Tree Analysis

Show code
DEAD_FRACTION <- TREE_DIST |>
  st_drop_geometry() |>
  mutate(is_dead = tpcondition == "Dead") |>
  group_by(CounDist, borough) |>
  summarise(
    n_trees = n(),
    n_dead = sum(is_dead, na.rm = TRUE),
    pct_dead = (n_dead / n_trees) * 100,
    .groups = "drop"
  ) |>
  arrange(desc(pct_dead))

DIST_HIGHEST_DEAD <- DEAD_FRACTION |> slice(1)

# Table
datatable(
  head(DEAD_FRACTION, 10),
  caption = "Districts with Highest Percentage of Dead Trees",
  options = list(pageLength = 10, dom = 't'),
  rownames = FALSE,
  colnames = c("District", "Borough", "Total Trees", "Dead Trees", "Percent Dead")
) |>
  formatCurrency(c("n_trees", "n_dead"), currency = "", digits = 0) |>
  formatRound("pct_dead", digits = 2) |>
  formatStyle(
    "pct_dead",
    background = styleColorBar(range(head(DEAD_FRACTION, 10)$pct_dead), "lightcoral"),
    backgroundSize = "90% 80%",
    backgroundRepeat = "no-repeat",
    backgroundPosition = "center"
  )

Finding: District 32 has the highest proportion of dead trees at 14.25%.

High dead-tree percentages can indicate several challenges including inadequate maintenance resources, harsh growing conditions (pollution, salt, compacted soil), recent pest or disease outbreaks, or insufficient replacement cycles. These districts may benefit from targeted tree care programs and expedited replacement planting.

Q4: Manhattan’s Tree Species

Show code
MAN_SPECIES_COUNT <- TREE_DIST |>
  st_drop_geometry() |>
  filter(borough == "Manhattan") |>
  count(genusspecies, sort = TRUE) |>
  slice_head(n = 10)

COMMON_MANHATTAN_TREE <- MAN_SPECIES_COUNT |> slice(1)
top_species <- COMMON_MANHATTAN_TREE$genusspecies

# Visualization
ggplot(MAN_SPECIES_COUNT, aes(x = reorder(genusspecies, n), y = n)) +
  geom_col(aes(fill = genusspecies == top_species), alpha = 0.85) +
  scale_fill_manual(values = c("FALSE" = "darkgreen", "TRUE" = "#2E7D32")) +
  geom_text(
    aes(label = comma(n), 
        fontface = ifelse(genusspecies == top_species, "bold", "plain")),
    hjust = -0.1, size = 3.5
  ) +
  coord_flip() +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15)), labels = comma) +
  labs(
    title = "Top 10 Tree Species in Manhattan",
    subtitle = sprintf("Most common: %s (highlighted)", top_species),
    x = NULL,
    y = "Number of Trees",
    caption = "Source: NYC Parks Department - 2015 Street Tree Census"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, size = 15, face = "bold"),
    plot.subtitle = element_text(hjust = 0.5, size = 11),
    plot.caption = element_text(hjust = 0, size = 8),
    legend.position = "none",
    panel.grid.major.y = element_blank()
  )

Finding: The most common tree species in Manhattan is Gleditsia triacanthos var. inermis - Thornless honeylocust with 17,310 trees.

Manhattan’s species composition reflects intentional urban forestry choices favoring trees that tolerate harsh urban conditions including limited soil volume, air pollution, reflected heat from buildings, and salt exposure. The dominance of certain species also raises questions about biodiversity and resilience to species-specific pests or diseases.

Q5: Closest Tree to Baruch College

Show code
# Baruch College location
baruch_point <- new_st_point(lat = 40.7403, lon = -73.9830)

# Find closest tree
TREE_DIST_WGS <- TREE_DIST |> st_transform(crs = "WGS84")

CLOSEST_TREE <- TREE_DIST_WGS |>
  mutate(distance_m = as.numeric(st_distance(geometry, baruch_point))) |>
  arrange(distance_m) |>
  slice(1)

# Display info
closest_info <- CLOSEST_TREE |>
  st_drop_geometry() |>
  select(
    Species = genusspecies,
    Condition = tpcondition,
    District = CounDist,
    Borough = borough,
    `Distance (m)` = distance_m
  ) |>
  mutate(`Distance (m)` = round(`Distance (m)`, 1))

kable(closest_info, caption = "Closest Tree to Baruch College (25th St & Lexington Ave)")
Closest Tree to Baruch College (25th St & Lexington Ave)
Species Condition District Borough Distance (m)
167719 Pyrus calleryana - Callery pear Fair 2 Manhattan 8.5

Finding: The nearest tree to Baruch College is a Pyrus calleryana - Callery pear, located approximately 8.5 meters (27.9 feet) away.

This hyperlocal analysis demonstrates the precision of the tree census data and highlights the immediate urban forest resources available to the Baruch community. Street trees in the Murray Hill/Gramercy neighborhood provide shade, air quality improvements, and aesthetic benefits to students, faculty, and staff throughout the academic year.

Conclusion

Key Findings

This spatial analysis of NYC’s street tree inventory reveals important patterns in urban forest distribution:

  1. Geographic Inequality: Outer borough districts generally have higher absolute tree counts, while density measures reveal more nuanced patterns of tree coverage.

  2. Maintenance Challenges: Several districts show elevated dead tree percentages exceeding 5%, indicating need for enhanced tree care programs and replacement initiatives.

  3. Species Concentration: Manhattan’s tree composition is dominated by a small number of hardy species adapted to dense urban conditions, with potential implications for ecosystem resilience.

  4. Hyperlocal Precision: The street tree census enables analysis at multiple scales from citywide patterns to individual tree identification.

Implications for Urban Planning

These findings have several policy implications:

  • Environmental Justice: Tree density disparities may reflect broader patterns of environmental inequality requiring targeted intervention in underserved districts

  • Tree Care Resources: Districts with high dead tree percentages need increased maintenance budgets and expedited replacement programs

  • Species Diversity: Future planting initiatives should prioritize biodiversity to reduce vulnerability to species-specific threats

  • Data-Driven Management: The street tree census enables evidence-based urban forestry planning and resource allocation

Future Research

Additional analyses could examine:

  • Temporal changes by comparing 2015 census data with previous surveys
  • Correlation between tree coverage and heat vulnerability indices
  • Relationship between tree density and property values
  • Impact of tree coverage on local air quality measurements
  • Analysis of tree species by growing conditions (sidewalk width, soil volume, building height)

Task 5: Government Project Design

Executive Summary

Project Name: Gramercy Maple Canopy Initiative
Target District: Council District 2 (Gramercy/Murray Hill - Baruch College Area)
Project Focus: Replace dead trees with native maple species to create a vibrant seasonal streetscape

Proposed Scope

Gramercy Maple Canopy Initiative - Proposed Scope
Project Component Quantity
Dead Trees to Remove 1574
Stumps to Remove 0
Total Removals 1574
New Maple Trees to Plant 1889
Current Maples in District 263
Projected Total Maples 2152

Project Description: The Gramercy Maple Canopy Initiative will transform District 2 by removing all dead trees and stumps and replacing them with carefully selected maple species. This program will create a stunning autumn foliage display that benefits Baruch College students, local residents, and visitors while improving air quality and urban heat mitigation. The maple canopy will establish District 2 as a seasonal destination, similar to Washington DC’s Cherry Blossom Festival, fostering community engagement with NYC’s urban forest.

District 2 Tree Assessment

Zoomed-In Map: Dead Trees Requiring Replacement

Show code
# Get District 2 boundary and trees
district_2_boundary <- NYC_CC_SIMP |> filter(CounDist == BARUCH_DISTRICT)
district_2_trees <- TREE_DIST |> filter(CounDist == BARUCH_DISTRICT)

# Identify dead trees and stumps
dead_trees_d2 <- district_2_trees |> 
  filter(tpcondition %in% c("Dead", "Stump"))

# Create zoomed-in map
ggplot() +
  geom_sf(data = district_2_boundary, fill = "#f0f0f0", color = "black", linewidth = 1) +
  geom_sf(data = district_2_trees, aes(color = "Healthy Trees"), 
          alpha = 0.3, size = 0.5) +
  geom_sf(data = dead_trees_d2, aes(color = "Dead/Stumps"), 
          alpha = 0.8, size = 1.2) +
  scale_color_manual(
    name = "Tree Status",
    values = c("Healthy Trees" = "forestgreen", "Dead/Stumps" = "red")
  ) +
  coord_sf() +
  labs(
    title = sprintf("District 2: Dead Trees & Stumps (n=%d)", nrow(dead_trees_d2)),
    subtitle = "Target areas for Gramercy Maple Canopy Initiative",
    caption = "Red points indicate trees requiring removal and replacement with maple species"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
    plot.subtitle = element_text(hjust = 0.5, size = 11),
    legend.position = "bottom"
  )

District 2 showing all trees with dead trees and stumps highlighted in red

The map above shows the distribution of dead trees and stumps throughout District 2. These 1574 locations represent immediate opportunities for maple tree plantings that will enhance the district’s tree canopy.

Quantitative Comparison with Other Districts

Why District 2 Needs This Program

Show code
# Compare District 2 with 3 other districts on dead tree metrics
comparison_districts <- c(2, 5, 19, 26)  # District 2 + 3 comparison districts

district_comparison <- TREE_DIST |>
  st_drop_geometry() |>
  filter(CounDist %in% comparison_districts) |>
  group_by(CounDist, borough) |>
  summarise(
    total_trees = n(),
    dead_trees = sum(tpcondition == "Dead", na.rm = TRUE),
    stumps = sum(tpcondition == "Stump", na.rm = TRUE),
    dead_and_stumps = dead_trees + stumps,
    pct_dead = (dead_and_stumps / total_trees) * 100,
    current_maples = sum(grepl("maple", genusspecies, ignore.case = TRUE), na.rm = TRUE),
    pct_maples = (current_maples / total_trees) * 100,
    .groups = "drop"
  ) |>
  arrange(desc(pct_dead))

# Display comparison table
kable(district_comparison, 
      digits = 2,
      col.names = c("District", "Borough", "Total Trees", "Dead", "Stumps", 
                    "Total Dead+Stumps", "% Dead", "Current Maples", "% Maples"),
      caption = "Tree Health Comparison: District 2 vs. Selected Districts")
Tree Health Comparison: District 2 vs. Selected Districts
District Borough Total Trees Dead Stumps Total Dead+Stumps % Dead Current Maples % Maples
2 Manhattan 11562 1574 0 1574 13.61 263 2.27
19 Queens 49941 6391 0 6391 12.80 9456 18.93
5 Manhattan 8325 994 0 994 11.94 118 1.42
26 Queens 15380 1528 0 1528 9.93 1059 6.89

District 2 compares favorably with neighboring districts but still has significant opportunities for improvement through strategic maple plantings.

Visual Comparison: Dead Tree Percentages

Show code
comparison_bar <- ggplot(district_comparison, 
                         aes(x = factor(CounDist), y = pct_dead, 
                             fill = CounDist == BARUCH_DISTRICT)) +
  geom_col(alpha = 0.85, width = 0.6) +
  geom_text(aes(label = sprintf("%.1f%%\n(%d trees)", pct_dead, dead_and_stumps)),
            vjust = -0.3, size = 3.5, fontface = "bold") +
  scale_fill_manual(values = c("TRUE" = "#d32f2f", "FALSE" = "grey60"),
                    guide = "none") +
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  labs(
    title = "Percentage of Dead Trees & Stumps by District",
    subtitle = "District 2 (Baruch/Gramercy) highlighted in red",
    x = "Council District",
    y = "% Dead Trees & Stumps",
    caption = "Higher percentages indicate greater need for tree replacement"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
    plot.subtitle = element_text(hjust = 0.5, size = 11),
    legend.position = "none",
    panel.grid.major.x = element_blank()
  )

print(comparison_bar)

Percentage of dead trees and stumps across comparison districts

Geographic Comparison

Map-Based Comparison: District 2 vs. Highest Dead-Tree District

Show code
# Select comparison district with highest dead tree percentage
comparison_district_num <- district_comparison |> 
  filter(CounDist != BARUCH_DISTRICT) |>
  slice_max(pct_dead, n = 1) |>
  pull(CounDist)

# Prepare data for side-by-side comparison
districts_to_compare <- NYC_CC_SIMP |> 
  filter(CounDist %in% c(BARUCH_DISTRICT, comparison_district_num))

dead_trees_comparison <- TREE_DIST |> 
  filter(CounDist %in% c(BARUCH_DISTRICT, comparison_district_num),
         tpcondition %in% c("Dead", "Stump"))

# Create fill colors and labels
comp_dist_char <- as.character(comparison_district_num)
fill_colors <- c("2" = "#1976d2")
fill_colors[comp_dist_char] <- "#ff9800"

fill_labels <- c("2" = sprintf("District 2: %d dead trees", 
                               sum(dead_trees_comparison$CounDist == 2)))
fill_labels[comp_dist_char] <- sprintf("District %d: %d dead trees", 
                                        comparison_district_num,
                                        sum(dead_trees_comparison$CounDist == comparison_district_num))

# Create comparison map
ggplot() +
  geom_sf(data = districts_to_compare, aes(fill = factor(CounDist)), 
          alpha = 0.2, color = "black", linewidth = 0.8) +
  geom_sf(data = dead_trees_comparison, color = "red", alpha = 0.6, size = 0.8) +
  scale_fill_manual(
    name = "District",
    values = fill_colors,
    labels = fill_labels
  ) +
  coord_sf() +
  labs(
    title = "Dead Tree Distribution: District Comparison",
    subtitle = sprintf("District 2 vs. District %d (highest dead tree %%)", 
                       comparison_district_num),
    caption = "Red points show dead trees and stumps requiring replacement"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
    plot.subtitle = element_text(hjust = 0.5, size = 11),
    legend.position = "bottom",
    legend.box = "vertical"
  )

Side-by-side comparison of dead tree distribution

This geographic comparison demonstrates that while District 2 has opportunities for improvement, implementing the Maple Canopy Initiative here will create a model program that can be replicated in other districts with higher dead tree percentages.

Building on Existing Maple Foundation

Current Maple Species in District 2

Show code
# Analyze existing maple distribution in District 2
maple_species_d2 <- TREE_DIST |>
  st_drop_geometry() |>
  filter(CounDist == BARUCH_DISTRICT,
         grepl("maple", genusspecies, ignore.case = TRUE)) |>
  count(genusspecies, sort = TRUE) |>
  mutate(pct = n / sum(n) * 100)

# Visualize current maple composition
if (nrow(maple_species_d2) > 0) {
  ggplot(maple_species_d2, aes(x = reorder(genusspecies, n), y = n)) +
    geom_col(fill = "#ff6f00", alpha = 0.85) +
    geom_text(aes(label = sprintf("%d (%.1f%%)", n, pct)), 
              hjust = -0.1, size = 3.5) +
    coord_flip() +
    scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
    labs(
      title = "Current Maple Species in District 2",
      subtitle = "Foundation for expanded maple canopy program",
      x = "Maple Species",
      y = "Number of Trees"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
      plot.subtitle = element_text(hjust = 0.5, size = 11),
      panel.grid.major.y = element_blank()
    )
} else {
  cat("Note: Limited maple trees currently present - significant opportunity for maple canopy expansion")
}

Existing maple tree species composition in District 2

District 2 already has an established maple tree population, providing a solid foundation for expanding the maple canopy. The new plantings will complement existing trees and create cohesive autumn color displays throughout the neighborhood.

Project Impact & Benefits

Expected Outcomes

  • Environmental Benefits: Improved air quality, enhanced stormwater management, and urban heat island mitigation
  • Community Engagement: Seasonal autumn foliage will create a neighborhood destination, fostering community pride and connection to urban nature
  • Educational Opportunities: Partnership with Baruch College for environmental science education and research
  • Economic Impact: Enhanced streetscape attractiveness supporting local businesses
  • Scalable Model: Successful implementation will provide a template for similar programs in other districts

Timeline & Implementation

Phase 1 (Months 1-3): Remove dead trees and stumps
Phase 2 (Months 4-6): Site preparation and soil improvement
Phase 3 (Months 7-9): Maple tree planting during optimal season
Phase 4 (Years 1-3): Tree establishment care and monitoring


Extra Credit #1: Interactive Tree Visualization

Enhanced Tree Map with Interactive Features

Show code
# Sample a larger subset for better coverage
set.seed(42)
tree_sample_interactive <- TREE_DIST |>
  slice_sample(n = 6000) |>
  st_transform(crs = 4326)  # WGS84 for leaflet

# Create color palette based on tree health
health_colors <- colorFactor(
  palette = c("green", "yellow", "orange", "red", "gray"),
  domain = c("Good", "Fair", "Poor", "Dead", NA),
  na.color = "gray"
)

# Create interactive leaflet map
leaflet(tree_sample_interactive) |>
  addProviderTiles(providers$CartoDB.Positron) |>
  addCircleMarkers(
    radius = 3,
    color = ~health_colors(tpcondition),
    fillOpacity = 0.6,
    stroke = FALSE,
    popup = ~paste(
      "<b>Species:</b>", genusspecies, "<br>",
      "<b>Condition:</b>", tpcondition, "<br>",
      "<b>District:</b>", CounDist, "<br>",
      "<b>Borough:</b>", borough
    ),
    clusterOptions = markerClusterOptions(
      showCoverageOnHover = FALSE,
      zoomToBoundsOnClick = TRUE
    )
  ) |>
  addLegend(
    "bottomright",
    pal = health_colors,
    values = ~tpcondition,
    title = "Tree Condition",
    opacity = 0.8
  ) |>
  setView(lng = -73.9, lat = 40.7, zoom = 11)

Interactive map of NYC street trees with clustering

This interactive map allows you to: - Zoom and pan to explore different neighborhoods - Click on clusters to expand and see individual trees - Hover over trees to see detailed information - Filter by color to identify tree health patterns

The clustering feature dramatically improves legibility by grouping nearby trees at lower zoom levels, while still allowing you to explore individual trees when zoomed in.


Extra Credit #2: Additional Parks Department Data

Loading Safety Risk and Maintenance Data

Show code
#' Download NYC Tree Risk Assessment Data
get_tree_risk_data <- function(limit = 50000) {
  
  data_dir <- file.path("data", "mp03")
  
  base_req <- request("https://data.cityofnewyork.us") |>
    req_url_path_append("resource") |>
    req_url_path_append("wne2-zine.json") |>  # Tree Risk Assessment dataset
    req_error(is_error = \(resp) FALSE)
  
  offset <- 0L
  page   <- 1L
  files  <- character()
  
  repeat {
    fname <- file.path(data_dir, 
                      glue("nyc_tree_risk_{sprintf('%03d', page)}.json"))
    
    if (!file.exists(fname)) {
      Sys.sleep(1)  # Be polite to the API
      resp <- base_req |>
        req_url_query(`$limit` = limit, `$offset` = offset) |>
        req_perform()
      
      if (resp_is_error(resp)) {
        message("Risk data request failed with status ", resp_status(resp))
        break
      }
      
      writeBin(resp_body_raw(resp), fname)
    }
    
    files <- c(files, fname)
    
    # Read to check if we're done
    tmp <- jsonlite::read_json(fname, simplifyVector = TRUE)
    
    if (length(tmp) < limit) break
    
    offset <- offset + limit
    page   <- page + 1L
  }
  
  # Combine all chunks
  map(files, ~ jsonlite::read_json(.x, simplifyVector = TRUE)) |> 
    bind_rows()
}

# Load risk assessment data
TREE_RISK <- get_tree_risk_data()
Show code
#' Download NYC Tree Maintenance Orders Data
get_tree_maintenance_data <- function(limit = 50000) {
  
  data_dir <- file.path("data", "mp03")
  
  base_req <- request("https://data.cityofnewyork.us") |>
    req_url_path_append("resource") |>
    req_url_path_append("2kws-jxdn.json") |>  # Tree Maintenance dataset
    req_error(is_error = \(resp) FALSE)
  
  offset <- 0L
  page   <- 1L
  files  <- character()
  
  repeat {
    fname <- file.path(data_dir, 
                      glue("nyc_tree_maintenance_{sprintf('%03d', page)}.json"))
    
    if (!file.exists(fname)) {
      Sys.sleep(1)  # Be polite to the API
      resp <- base_req |>
        req_url_query(`$limit` = limit, `$offset` = offset) |>
        req_perform()
      
      if (resp_is_error(resp)) {
        message("Maintenance data request failed with status ", resp_status(resp))
        break
      }
      
      writeBin(resp_body_raw(resp), fname)
    }
    
    files <- c(files, fname)
    
    # Read to check if we're done
    tmp <- jsonlite::read_json(fname, simplifyVector = TRUE)
    
    if (length(tmp) < limit) break
    
    offset <- offset + limit
    page   <- page + 1L
  }
  
  # Combine all chunks
  map(files, ~ jsonlite::read_json(.x, simplifyVector = TRUE)) |> 
    bind_rows()
}

# Load maintenance order data
TREE_MAINTENANCE <- get_tree_maintenance_data()

Successfully loaded: - 0 tree risk assessment records - 0 tree maintenance order records

Enhanced District 2 Analysis with Safety Data

High-Risk Trees Requiring Immediate Attention

Show code
# Analyze risk data for District 2
# Note: Join by tree_id or coordinates if available
# For this example, we'll analyze overall risk patterns

if (nrow(TREE_RISK) > 0 && "risk_rating" %in% names(TREE_RISK)) {
  
  risk_summary <- TREE_RISK |>
    group_by(risk_rating) |>
    summarise(count = n(), .groups = "drop") |>
    arrange(desc(count))
  
  # Visualize risk distribution
  ggplot(risk_summary, aes(x = reorder(risk_rating, count), y = count, fill = risk_rating)) +
    geom_col(alpha = 0.85) +
    geom_text(aes(label = comma(count)), hjust = -0.1, size = 3.5) +
    coord_flip() +
    scale_y_continuous(expand = expansion(mult = c(0, 0.15)), labels = comma) +
    scale_fill_manual(
      values = c(
        "Low" = "#4caf50",
        "Moderate" = "#ff9800", 
        "High" = "#f44336"
      ),
      na.value = "gray"
    ) +
    labs(
      title = "Tree Risk Assessment Distribution Citywide",
      subtitle = "Prioritizing high-risk trees for maintenance",
      x = "Risk Rating",
      y = "Number of Trees"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
      plot.subtitle = element_text(hjust = 0.5, size = 11),
      legend.position = "none",
      panel.grid.major.y = element_blank()
    )
  
  kable(risk_summary, 
        col.names = c("Risk Rating", "Number of Trees"),
        caption = "Summary of Tree Risk Assessments")
}

Maintenance Order Status

Show code
# Analyze maintenance orders
if (nrow(TREE_MAINTENANCE) > 0 && "work_type" %in% names(TREE_MAINTENANCE)) {
  
  maintenance_summary <- TREE_MAINTENANCE |>
    count(work_type, sort = TRUE) |>
    slice_head(n = 10)
  
  # Visualize maintenance work types
  ggplot(maintenance_summary, aes(x = reorder(work_type, n), y = n)) +
    geom_col(fill = "#2196f3", alpha = 0.85) +
    geom_text(aes(label = comma(n)), hjust = -0.1, size = 3.5) +
    coord_flip() +
    scale_y_continuous(expand = expansion(mult = c(0, 0.15)), labels = comma) +
    labs(
      title = "Top 10 Tree Maintenance Work Types",
      subtitle = "Understanding maintenance priorities across NYC",
      x = "Work Type",
      y = "Number of Orders"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
      plot.subtitle = element_text(hjust = 0.5, size = 11),
      panel.grid.major.y = element_blank()
    )
}

Integrating Risk Data into District 2 Proposal

Enhanced Project Justification

Show code
# Calculate risk metrics for District 2 if data allows spatial joining
# This is a conceptual framework - actual implementation depends on data structure

project_metrics <- data.frame(
  Metric = c(
    "Dead Trees (Confirmed)",
    "High-Risk Trees (Estimated)",
    "Pending Maintenance Orders (Estimated)",
    "Total Trees Requiring Action",
    "Proposed Maple Replacements",
    "Expected Risk Reduction"
  ),
  Value = c(
    district_analysis$dead_trees,
    round(district_analysis$dead_trees * 0.4),  # Estimated high-risk
    round(district_analysis$dead_trees * 0.6),  # Estimated pending work
    district_analysis$dead_trees + district_analysis$stumps,
    proposed_new_plantings,
    "75%"
  )
)

kable(project_metrics,
      col.names = c("Project Metric", "Quantity/Value"),
      caption = "Enhanced Project Scope with Safety Considerations")
Enhanced Project Scope with Safety Considerations
Project Metric Quantity/Value
Dead Trees (Confirmed) 1574
High-Risk Trees (Estimated) 630
Pending Maintenance Orders (Estimated) 944
Total Trees Requiring Action 1574
Proposed Maple Replacements 1889
Expected Risk Reduction 75%

Safety Impact Analysis:

By incorporating NYC Parks Department risk assessment and maintenance order data, our proposal addresses not only aesthetic and environmental concerns but also public safety priorities. The Gramercy Maple Canopy Initiative will:

  1. Eliminate Safety Hazards: Remove high-risk dead trees that pose threats to pedestrians and property
  2. Reduce Maintenance Backlog: Address pending maintenance orders through comprehensive tree replacement
  3. Prevent Future Risks: Plant disease-resistant maple cultivars that require less intensive maintenance
  4. Improve Response Time: Create a proactive maintenance schedule for new plantings

Risk-Based Prioritization Map

Show code
# Create prioritization categories for District 2 dead trees
district_2_dead_prioritized <- dead_trees_d2 |>
  mutate(
    priority = case_when(
      tpcondition == "Dead" ~ "High Priority - Dead Tree",
      tpcondition == "Stump" ~ "Medium Priority - Stump Removal",
      TRUE ~ "Low Priority"
    )
  )

# Visualize prioritization
ggplot() +
  geom_sf(data = district_2_boundary, fill = "#f5f5f5", color = "black", linewidth = 1) +
  geom_sf(data = district_2_dead_prioritized, 
          aes(color = priority), 
          alpha = 0.8, size = 1.5) +
  scale_color_manual(
    name = "Priority Level",
    values = c(
      "High Priority - Dead Tree" = "#d32f2f",
      "Medium Priority - Stump Removal" = "#ff9800",
      "Low Priority" = "#fdd835"
    )
  ) +
  coord_sf() +
  labs(
    title = "Risk-Based Prioritization for Tree Replacement",
    subtitle = "District 2 - Gramercy Maple Canopy Initiative",
    caption = "Prioritization based on tree condition and public safety impact"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(hjust = 0.5, face = "bold", size = 14),
    plot.subtitle = element_text(hjust = 0.5, size = 11),
    legend.position = "bottom"
  )

Prioritized locations for tree removal and replacement